/**
 * Copyright 2019-2024 覃海林(qinhaisenlin@163.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ 

package com.qinhailin.pub.login.service;

import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Random;

import javax.servlet.http.HttpServletRequest;

import com.jfinal.aop.Inject;
import com.jfinal.kit.Ret;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.ehcache.CacheKit;
import com.qinhailin.pub.login.exception.LoginException;
import com.qinhailin.common.entity.impl.LoginUserImpl;
import com.qinhailin.common.kit.Md5Kit;
import com.qinhailin.common.model.Session;
import com.qinhailin.common.model.SysUser;
import com.qinhailin.common.visit.Visitor;
import com.qinhailin.common.visit.impl.VisitorImpl;
import com.qinhailin.common.config.WebContant;
import com.qinhailin.portal.core.service.SysUserService;

/**
 * @author QinHaiLin
 *
 */
public class LoginService {

	@Inject
	SysUserService sysUserService;
	
	// 存放登录用户的 cacheName
	public static final String loginAccountCacheName = "loginAccount";

	// "jfinalLayuiId" 仅用于 cookie 名称，其它地方如 cache 中全部用的 "sessionId" 来做 key
	public static final String sessionIdName = "jfinalLayuiId";

	/**
	 * 登陆验证
	 * 
	 * @param userCode
	 * @param password
	 * @param req
	 * @return
	 * @throws LoginException
	 * @author QinHaiLin
	 * @date 2018年10月15日
	 */
	public Ret aopLogin(String userCode, String password, HttpServletRequest req) throws LoginException {
		//验证数据
		if ((userCode == null) || (userCode.trim().length() == 0) || (password == null)
				|| (password.trim().length() == 0)) {
			throw new LoginException("请输入用户名和密码");
		}
		//验证用户
		SysUser user = sysUserService.findByUserCode(userCode.toLowerCase());
		if (user == null) {
			checkUserCode(req, userCode);
		}		
		//验证权限
		if ((user.getAllowLogin() != null) && (user.getAllowLogin() != WebContant.allowLogin)) {
			throw new LoginException("没有登录权限，请联系管理员");
		}		
		//验证码
		checkVerifyCode(user, req);		
		// 验证密码
		if (user.getPasswd().equals(Md5Kit.md5(password))) {
			String keepLogin=req.getParameter("keepLogin");
			if(keepLogin!=null)
				user.setKeepLogin(Integer.parseInt(keepLogin));
			return returnVistor(user, req);
		}
		//验证失败，记录失败次数，达到fail_num值将锁定账号
		loginFail(user);
			
		return null;
	}
	
	/**
	 * 标记登陆账号信息，锁定账号
	 * @param req
	 * @param userCode
	 * @throws LoginException
	 */
	public void checkUserCode(HttpServletRequest req,String userCode)throws LoginException{
		if(WebContant.isLockUser==1){
			Object num=req.getSession().getAttribute(userCode.toLowerCase());
			if(num!=null){
				int n=Integer.parseInt(num.toString())+1;
				req.getSession().setAttribute(userCode.toLowerCase(), n);
				if(n>=WebContant.failNum){
					throw new LoginException("账号已被锁定24小时，请联系管理员");
				}else{
					throw new LoginException("账号密码错误 "+n+" 次，第"+WebContant.failNum+"次错误账号将被锁定24小时");
				}
			}else{
				req.getSession().setAttribute(userCode.toLowerCase(), 1);
				throw new LoginException("账号密码错误 1 次，第"+WebContant.failNum+"次错误账号将被锁定24小时");
			}		
		}else{
			throw new LoginException("账号密码错误");
		}
	}

	/**
	 * 检查验证码
	 * 
	 * @param user
	 * @param req
	 * @throws LoginException
	 * @author QinHaiLin
	 * @date 2018年10月22日
	 */
	private void checkVerifyCode(SysUser user, HttpServletRequest req) throws LoginException {
		Date allowLogTime = user.getAllowLoginTime();
		if (allowLogTime != null) {
			Date nowDate = new Date();
			Calendar cal = Calendar.getInstance();
			Calendar cal2 = Calendar.getInstance();
			cal.setTime(allowLogTime);
			cal2.setTime(nowDate);
			float f = cal.getTimeInMillis() - cal2.getTimeInMillis();
			String verifyCode = req.getParameter("verifyCode");
			// 验证码验证
			if (f < 0||StrKit.notBlank(verifyCode)) {	
				if(user.getFailureNumber()>0||StrKit.notBlank(verifyCode)){
					String code = (String) req.getSession().getAttribute("verifyCode");
					// 更换验证码！防止暴力破解
					Random random = new Random();
					String sRand = "";
					for (int i = 0; i < 4; i++) {
						String rand = String.valueOf(random.nextInt(10));
						sRand += rand;
					}
					req.getSession().setAttribute("verifyCode", sRand);
					if ((verifyCode == null) || !verifyCode.equalsIgnoreCase(code)) {
						throw new LoginException("验证码错误，请重新输入");
					}					
				}
			}else{
				throw new LoginException("账号已被锁定24小时，请联系管理员");
			}
			
		}
	}

	/**
	 * 返回登录者信息
	 * 
	 * @param user
	 * @param req
	 * @return
	 * @throws LoginException
	 * @author QinHaiLin
	 * @date 2018年10月22日
	 */
	public Ret returnVistor(SysUser user, HttpServletRequest req) throws LoginException {
		// 记录最后一次登录时间
		user.setAllowLoginTime(new Date());
		user.setFailureNumber(0);
		user.update();
		return saveSession(user);
	}
	
	/**
	 * 获取登陆对象
	 * @param user
	 * @param ip
	 * @return
	 */
	public Visitor getVistor(SysUser user){
		LoginUserImpl loginUser = new LoginUserImpl();
		loginUser.setId(user.getId());
		loginUser.setUserCode(user.getUserCode());
		loginUser.setUserName(user.getUserName());
		loginUser.setOrgId(user.getOrgId());
		VisitorImpl vistor = new VisitorImpl(loginUser);
		vistor.setType(user.getSex());
		// 权限
		Map<String, Boolean> funcMap = sysUserService.getUserFuncMap(user.getUserCode());
		vistor.setFuncMap(funcMap);
		vistor.setTheme(user.getTheme());
		return vistor;
	}

	/**
	 * 登陆失败
	 * 
	 * @param user
	 * @throws LoginException
	 * @author QinHaiLin
	 * @date 2018年10月22日
	 */
	private void loginFail(SysUser user) throws LoginException {
		int failureNumber = user.getFailureNumber() == null ? 0
				: user.getFailureNumber();
		user.setFailureNumber(failureNumber + 1);
		String msg = "帐号或密码错误";
		if(WebContant.isLockUser==1){
			if (failureNumber >= WebContant.failNum-1) {//错误次数  
				Date now = new Date();
				Calendar cal = Calendar.getInstance();
				cal.setTime(now);
				//锁定账号1天
				cal.add(Calendar.DATE, 1);
				user.setAllowLoginTime(cal.getTime());
				msg = "账号已被锁定24小时，请联系管理员";
			}else{
				msg="密码或错误 "+user.getFailureNumber()+" 次，第"+WebContant.failNum+"次错误账号将被锁定24小时";
			}		
		}
		user.update();
		throw new LoginException(msg);
	}
	
	/**
	 * 保存session到数据库
	 * @param user
	 * @return  传递给控制层的 cookie最大存活时间
	 */
	public Ret saveSession(SysUser user) throws LoginException {
		// 如果用户勾选保持登录，暂定过期时间为 1 年，否则为 12 小时，单位为秒
		boolean keepLogin=user.getKeepLogin()==1 ? true : false;
		long liveSeconds =  keepLogin ? 1 * 360 * 24 * 60 * 60 : 12 * 60 * 60;
		// 传递给控制层的 cookie
		int maxAgeInSeconds = (int)(keepLogin ? liveSeconds : -1);
		// expireAt 用于设置 session 的过期时间点，需要转换成毫秒
		long expireAt = System.currentTimeMillis() + (liveSeconds * 1000);
		// 保存登录 session 到数据库
		Session session = new Session();
		String sessionId = StrKit.getRandomUUID();
		session.setId(sessionId);
		session.setUserCode(user.getUserCode());
		session.setExpiretTime(expireAt);
		user.setSessionId(sessionId); 
		if(!session.save())
			throw new LoginException("保存 session 到数据库失败，请联系管理员");
		CacheKit.put(loginAccountCacheName, sessionId, user);// 保存一份 sessionId 到 loginAccount 备用
		return Ret.ok(sessionIdName, sessionId)
				.set(loginAccountCacheName, user)
				.set("maxAgeInSeconds", maxAgeInSeconds);   // 用于设置 cookie 的最大存活时间
	}
	
	public SysUser getLoginAccountWithSessionId(String sessionId) {
		return CacheKit.get(loginAccountCacheName, sessionId);
	}
	
	/**
	 * 通过 sessionId 获取登录用户信息
	 * sessoin表结构：session(id, user_code, expire_time)
	 *
	 * 1：先从缓存里面取，如果取到则返回该值，如果没取到则从数据库里面取
	 * 2：在数据库里面取，如果取到了，则检测是否已过期，如果过期则清除记录，
	 *     如果没过期则先放缓存一份，然后再返回
	 */
	public SysUser loginWithSessionId(String sessionId, String loginIp) {
		Session session = Session.dao.findById(sessionId);
		if (session == null) {      // session 不存在
			return null;
		}
		if (session.isExpired()) {  // session 已过期
			session.delete();		// 被动式删除过期数据，此外还需要定时线程来主动清除过期数据
			return null;
		}

		SysUser loginAccount = sysUserService.findByUserCode(session.getUserCode());
		// 找到 loginAccount 并且 是正常状态 才允许登录
		if (loginAccount != null && loginAccount.getAllowLogin()==0) {   // 移除 password 与 salt 属性值
			loginAccount.setSessionId(sessionId);                        // 保存一份 sessionId 到 loginAccount 备用
			CacheKit.put(loginAccountCacheName, sessionId, loginAccount);			
			return loginAccount;
		}
		return null;
	}
	
	/**
	 * 退出登录
	 */
	public void logout(String sessionId) {
		if (sessionId != null) {
			CacheKit.remove(loginAccountCacheName, sessionId);
			Session.dao.deleteById(sessionId);
		}
	}

	/**
	 * 从数据库重新加载登录账户信息
	 */
	public void reloadLoginAccount(SysUser loginAccountOld) {
		String sessionId = loginAccountOld.getSessionId();
		SysUser user = sysUserService.findByUserCode(loginAccountOld.getUserCode());
		user.setSessionId(sessionId);// 保存一份 sessionId 到 loginAccount 备用
		// 集群方式下，要做一通知其它节点的机制，让其它节点使用缓存更新后的数据，
		// 将来可能把 account 用 id : obj 的形式放缓存，更新缓存只需要 CacheKit.remove("account", id) 就可以了，
		// 其它节点发现数据不存在会自动去数据库读取，所以未来可能就是在 AccountService.getById(int id)的方法引入缓存就好
		// 所有用到 account 对象的地方都从这里去取
		CacheKit.put(loginAccountCacheName, sessionId, user);
	}
}
